package bubblechart.server;

import hirondelle.date4j.DateTime;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.apache.thrift.TException;
import org.apache.thrift.TSerializer;
import org.apache.thrift.protocol.TSimpleJSONProtocol;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRendererState;
import org.jfree.data.xy.DefaultXYDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;

import visual.Bubble;
import visual.ColorGenerator;
import visual.Meta;
import visual.Plot;
import visual.Timeline;
import data.BubbleData;

@SuppressWarnings("serial")
public class BallsServlet extends HttpServlet {
	
	private Connection conn;
	
	private PreparedStatement findNominalPs;
	private PreparedStatement findNumericalPs;
	
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			resp.getWriter().append(generateHtml(
					req.getParameter("definition")==null?
							"":req.getParameter("definition")));
		} catch (TException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}


	public String generateHtml(String definition) throws TException, IOException, SQLException {
		
		// First key is the grouping date
		// Second key is the category for color
		// Third key is the category for bubbles
		Map<String,Map<String,BubbleData>> bubbleMap = new HashMap<String,Map<String,BubbleData>>();
		SortedSet<String> allTimes = new TreeSet<String>();
		Set<String> allBubbles = new HashSet<String>();
		
		String nominalIndicator = null;
		String xIndicator = null;
		String yIndicator = null;
		String sizeIndicator = null;
		Map<String,Set<String>> inclusionFilter = new HashMap<String,Set<String>>();
		Map<String,Set<String>> exclusionFilter = new HashMap<String,Set<String>>();
		String[] lines = definition.split("\\r?\\n");
		for (String line: lines) {
			int spaceIndex = line.indexOf(' ');
			if (spaceIndex != -1) {
				String verb = line.substring(0, spaceIndex);
				String noun = line.substring(spaceIndex+1);
				if (verb.startsWith("Exclude")) {
					verb = verb.substring("Exclude".length());
					Set<String> exclusions = exclusionFilter.get(verb);
					if (exclusions == null) {
						exclusionFilter.put(verb, exclusions = new HashSet<String>());
					}
					exclusions.add(noun);
				} else if ("Bubble".equals(verb)) {
					nominalIndicator = noun;
				} else if ("X".equals(verb)) {
					xIndicator = noun;
				} else if ("Y".equals(verb)) {
					yIndicator = noun;
				} else if ("Size".equals(verb)) {
					sizeIndicator = noun;
				} else {
					Set<String> inclusions = inclusionFilter.get(verb);
					if (inclusions == null) {
						inclusionFilter.put(verb, inclusions = new HashSet<String>());
					}
					inclusions.add(noun);
				}
			}
		}
		if (nominalIndicator == null) {
			nominalIndicator = "Country";
		}
		if (xIndicator == null) {
			xIndicator = "Income";
		}
		if (yIndicator == null) {
			yIndicator = "Life expectancy";
		}
		if (sizeIndicator == null) {
			sizeIndicator = "Population";
		}
		
		Set<Integer> exclusionSet = lookupFilterIds(exclusionFilter);
		Set<Integer> inclusionSet = lookupFilterIds(inclusionFilter);
		// Find id of the chosen nominal indicator
		findNominalPs.setString(1, nominalIndicator);
		ResultSet findNominalRs = findNominalPs.executeQuery();
		//TODO: Handle the case where specified indicator does not exist
		findNominalRs.next();
		int nominalIndicatorId = findNominalRs.getInt(1);
		// Find ids of 3 numerical indicators - for X, Y axes and for size of the bubble
		int xIndicatorId = findNumericalIndicatorId(xIndicator);
		int yIndicatorId = findNumericalIndicatorId(yIndicator);
		int sizeIndicatorId = findNumericalIndicatorId(sizeIndicator);
		
		// Produce list of knot ids that satisfy exclusion and inclusion conditions
		List<Integer> knotIds = new LinkedList<Integer>();
		PreparedStatement knotNominalIdPs = conn.prepareStatement("SELECT knot_id,nominal_value_id FROM knot_nominal ORDER BY knot_id");
		ResultSet knotNominalIdRs = knotNominalIdPs.executeQuery();
		if (knotNominalIdRs.next()) {
			int currentKnotId = knotNominalIdRs.getInt(1);
			boolean included = inclusionSet.isEmpty() || inclusionSet.contains(knotNominalIdRs.getInt(2));
			boolean excluded = exclusionSet.contains(knotNominalIdRs.getInt(2));
			while (knotNominalIdRs.next()) {
				int knotId = knotNominalIdRs.getInt(1);
				if (knotId != currentKnotId) {
					// Previous knot is over
					if (included && !excluded) {
						knotIds.add(currentKnotId);
					}
					currentKnotId = knotId;
					included = inclusionSet.isEmpty() || inclusionSet.contains(knotNominalIdRs.getInt(2));
					excluded = exclusionSet.contains(knotNominalIdRs.getInt(2));
				} else {
					included |= inclusionSet.contains(knotNominalIdRs.getInt(2));
					excluded |= exclusionSet.contains(knotNominalIdRs.getInt(2));
				}
			}
			if (included && !excluded) {
				knotIds.add(currentKnotId);
			}
		}
		
		int pointCount = 0;
		PreparedStatement knotPs = conn.prepareStatement("SELECT t FROM knot WHERE id=?");
		// Find the value of the nominal indicator
		PreparedStatement knotNominalPs = conn.prepareStatement("SELECT nv.value FROM nominal_value nv, knot_nominal kn WHERE kn.knot_id=? AND kn.nominal_value_id=nv.id AND nv.nominal_id=?");
		// Find 3 numerical indicators we require
		PreparedStatement knotNumericalPs = conn.prepareStatement(
				"SELECT numerical_id, value FROM knot_numerical WHERE knot_id=? AND numerical_id IN (?,?,?)");
		int zeroValuesCounter = 0;
		int oneValueCounter = 0;
		int twoValuesCounter = 0;
		int threeValuesCounter = 0;
		int xCounter = 0;
		int yCounter = 0;
		int sCounter = 0;
		for (int knotId: knotIds) {
			knotPs.setInt(1, knotId);
			ResultSet knotRs = knotPs.executeQuery();
			knotRs.next();
			DateTime timestamp = DateTime.forInstant(knotRs.getTimestamp(1).getTime(), TimeZone.getTimeZone("UTC"));
			String timestampString = timestamp.format("YYYY");
			// Find the value of the nominal indicator
			knotNominalPs.setInt(1, knotId);
			knotNominalPs.setInt(2, nominalIndicatorId);
			ResultSet knotNominalRs = knotNominalPs.executeQuery();
			knotNominalRs.next();
			String nominalValue = knotNominalRs.getString(1);
			knotNominalRs.close();
			// Get all numerical indicators for this knot
			knotNumericalPs.setInt(1, knotId);
			knotNumericalPs.setInt(2, xIndicatorId);
			knotNumericalPs.setInt(3, yIndicatorId);
			knotNumericalPs.setInt(4, sizeIndicatorId);
			BubbleData data = new BubbleData();
			ResultSet knotNumericalRs = knotNumericalPs.executeQuery();
			int values = 0;
			while (knotNumericalRs.next()) {
				int numericalId = knotNumericalRs.getInt(1);
				if (numericalId == xIndicatorId) {
					data.x = knotNumericalRs.getDouble(2);
					values++;
					xCounter++;
				} else if (numericalId == yIndicatorId) {
					data.y = knotNumericalRs.getDouble(2);
					values++;
					yCounter++;
				} else if (numericalId == sizeIndicatorId) {
					data.s = knotNumericalRs.getDouble(2);
					values++;
					sCounter++;
				}
			}
			switch (values) {
			case 0: zeroValuesCounter++; break;
			case 1: oneValueCounter++; break;
			case 2: twoValuesCounter++; break;
			case 3: threeValuesCounter++; break;
			}
			// For now filter out the points where not all
			// data are present, but ideally need to
			// perform interpolation for the missing data
			if (values == 3) {
			allTimes.add(timestampString);
			allBubbles.add(nominalValue);
			Map<String, BubbleData> m = bubbleMap.get(timestampString);
			if (m == null) {
				bubbleMap.put(timestampString, m = new HashMap<String, BubbleData>());
			}
			m.put(nominalValue, data);
			pointCount++;
			}
		}
		knotPs.close();
		knotNominalPs.close();
		knotNumericalPs.close();
		System.out.println("Knots with 0 values: " + zeroValuesCounter);
		System.out.println("Knots with 1 value: " + oneValueCounter);
		System.out.println("Knots with 2 values: " + twoValuesCounter);
		System.out.println("Knots with 3 values: " + threeValuesCounter);
		System.out.println("Knots with x value: " + xCounter);
		System.out.println("Knots with y value: " + yCounter);
		System.out.println("Knots with s value: " + sCounter);
		
		// Creating metadata
		List<String> bubbleList = new ArrayList<String>(allBubbles);
		Meta m = new Meta();
		m.s = sizeIndicator;
		double smin = Double.POSITIVE_INFINITY;
		double smax = Double.NEGATIVE_INFINITY;
		m.x = xIndicator;
		m.y = yIndicator;
		m.b = bubbleList;
		ColorGenerator colorGen = new ColorGenerator(new TreeSet<Color>(
				Collections.singletonList(new Color(150,150,150))));
		m.c = new ArrayList<String>();
		for (int i=0;i<bubbleList.size();i++) {
			Color col = colorGen.next();
			m.c.add(""+col.getRed()+","+col.getGreen()+","+col.getBlue());
		}
		m.t = new ArrayList<String>(allTimes);
		
		double[][] data = new double[2][pointCount];
		double[] sizes = new double[pointCount];
		int[] indices = new int[pointCount];
		final int[] xs = new int[pointCount];
		final int[] ys = new int[pointCount];
		int point = 0;
		for (String time: allTimes) {
			Map<String,BubbleData> plot = bubbleMap.get(time);
			if (plot == null) {
				plot = new HashMap<String,BubbleData>();
			}
			for (int i=0;i<bubbleList.size();i++) {
				String bubbleName = bubbleList.get(i);
				BubbleData bubbleData = plot.get(bubbleName);
				if (bubbleData != null) {
					if (bubbleData.s > smax) {
						smax = bubbleData.s;
					}
					if (bubbleData.s < smin) {
						smin = bubbleData.s;
					}
					data[0][point] = bubbleData.x;
					data[1][point] = bubbleData.y;
					sizes[point] = bubbleData.s;
					indices[point] = i;
					point++;
				}
			}
		}
		System.out.println("S min = " + smin);
		System.out.println("S max = " + smax);
		DefaultXYDataset dataSet = new DefaultXYDataset();
		dataSet.addSeries("1", data);
		NumberAxis xAxis = new NumberAxis(xIndicator);
		xAxis.setAutoRangeIncludesZero(false);
		LogarithmicAxis yAxis = new LogarithmicAxis(yIndicator);
		yAxis.setAllowNegativesFlag(true);
		yAxis.setStrictValuesFlag(false);
		yAxis.setAutoRangeIncludesZero(false);
		AbstractXYItemRenderer renderer = new AbstractXYItemRenderer() {
			
			@Override
			public void drawItem(Graphics2D g2, XYItemRendererState state,
					Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
					ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
					int series, int item, CrosshairState crosshairState, int pass) {
				double x = dataset.getXValue(series, item);
				double y = dataset.getYValue(series, item);
				RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
				RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
				xs[item] = (int)Math.round(domainAxis.valueToJava2D(x, dataArea,
						domainAxisLocation));
				ys[item] = (int)Math.round(rangeAxis.valueToJava2D(y, dataArea,
						rangeAxisLocation));
			}
		};
		double minRadius = 5;
		double maxRadius = 75;
		double a1 = (maxRadius*maxRadius-minRadius*minRadius)/(smax-smin);
		double a2 = minRadius*minRadius;

		XYPlot xyPlot = new XYPlot(dataSet, xAxis, yAxis, renderer);
		xyPlot.setOrientation(PlotOrientation.VERTICAL);
		xyPlot.setAxisOffset(new RectangleInsets(75, 75, 75, 75));
		JFreeChart chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT,
				xyPlot, false);
		BufferedImage image = chart.createBufferedImage(800, 600);

		Map<Integer, Color> generatedColors = new HashMap<Integer, Color>();
		
		Timeline tl = new Timeline();
		tl.p = new ArrayList<Plot>();
		point = 0;
		for (String time: allTimes) {
			Map<String,BubbleData> plot = bubbleMap.get(time);
			if (plot == null) {
				plot = new HashMap<String,BubbleData>();
			}
			Plot pt = new Plot();
			tl.p.add(pt);
			pt.b = new ArrayList<Bubble>();
			for (int i=0;i<bubbleList.size();i++) {
				String bubbleName = bubbleList.get(i);
				BubbleData bubbleData = plot.get(bubbleName);
				if (bubbleData != null) {
					Bubble bubble = new Bubble();
					bubble.i = i;
					bubble.x = xs[point];
					bubble.y = ys[point];
					bubble.s = (int)Math.round(Math.sqrt((sizes[point] - smin)*a1+a2));
					point++;
					Color col = generatedColors.get(i);
					if (col == null) {
						col = colorGen.next();
						generatedColors.put(i, col);
					}
					pt.b.add(bubble);
				}
			}
			Collections.sort(pt.b, new Comparator<Bubble>() {
				@Override
				public int compare(Bubble o1, Bubble o2) {
					return Double.compare(o2.s, o1.s);
				}
			});
		}
		
		
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		PrintStream ps = new PrintStream(bos);
		
		TSerializer serializer = new TSerializer(new TSimpleJSONProtocol.Factory());
		LineNumberReader r = new LineNumberReader(new FileReader("resources/visual.html.top"));
		String line;
		while ((line = r.readLine()) != null) {
			ps.println(line);
		}
		r.close();
		
		ps.println("var timeline=");
		ps.println(serializer.toString(tl));
		ps.println("var metadata=");
		ps.println(serializer.toString(m));
		ps.print("var img_src = 'data:image/png;base64,");
		
		Iterator<ImageWriter> writers = ImageIO
				.getImageWritersByFormatName("png");
		ImageWriter writer = (ImageWriter) writers.next();
		ByteArrayOutputStream bytes = new ByteArrayOutputStream();
		ImageOutputStream ios = ImageIO.createImageOutputStream(bytes);
		writer.setOutput(ios);
		writer.write(image);
		ios.close();
		ps.write(Base64.encodeBase64URLSafe(bos.toByteArray()));		
		ps.println("';");
		
		r = new LineNumberReader(new FileReader("resources/visual.html.bottom"));
		while ((line = r.readLine()) != null) {
			ps.println(line.replace("DEFINITION", definition));
		}
		r.close();
		ps.close();
		return bos.toString();
	}


	private int findNumericalIndicatorId(String indicatorName)
			throws SQLException {
		ResultSet findNumericalRs;
		findNumericalPs.setString(1, indicatorName);
		findNumericalRs = findNumericalPs.executeQuery();
		//TODO: Handle the case where specified indicator does not exist
		findNumericalRs.next();
		return findNumericalRs.getInt(1);
	}


	private Set<Integer> lookupFilterIds(Map<String, Set<String>> filter)
			throws SQLException {
		PreparedStatement findNominalValuePs = conn.prepareStatement("SELECT id FROM nominal_value WHERE nominal_id=? AND value=?");
		Set<Integer> idSet = new HashSet<Integer>();
		// Produce list of knot ids that satisfy the exclusion and inclusion conditions
		for (Map.Entry<String, Set<String>> filterEntry: filter.entrySet()) {
			String nominalName = filterEntry.getKey();
			findNominalPs.setString(1, nominalName);
			ResultSet findNominalRs = findNominalPs.executeQuery();
			if (findNominalRs.next()) {
				int nominalIndicatorId = findNominalRs.getInt(1);
				for (String value: filterEntry.getValue()) {
					findNominalValuePs.setInt(1, nominalIndicatorId);
					findNominalValuePs.setString(2, value);
					ResultSet findNominalValueRs = findNominalValuePs.executeQuery();
					if (findNominalValueRs.next()) {
						int nominalValueId = findNominalValueRs.getInt(1);
						idSet.add(nominalValueId);
					}
					findNominalValueRs.close();
				}
			}
			findNominalRs.close();
		}
		findNominalValuePs.close();
		return idSet;
	}

	@Override
	public void destroy() {
		try {
			findNominalPs.close();
			findNumericalPs.close();
			conn.close();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		super.destroy();
	}

	@Override
	public void init() throws ServletException {
		super.init();
		try {
			Class.forName("org.h2.Driver");
			conn = DriverManager.getConnection("jdbc:h2:resources/bubble_data", "sa", "");
			findNominalPs = conn.prepareStatement("SELECT id FROM nominal WHERE name=?");
			findNumericalPs = conn.prepareStatement("SELECT id FROM numerical WHERE name=?");
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
}
